﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Windows.Forms;

namespace AutoExtractor
{
    public partial class AutoExtractor : Form
    {
        private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
        private const string password_error_pattern = "CRC failed in";
        private const string cannot_find_volume = "Cannot find volume";
        private string srcDir, dstDir, dstRoot;
        private string movieFileExtensions;
        private String[] dstRootDirs;
        private const string UNRAR_COMMAND = "Unrar.exe";
        private const string UNZIP_COMMAND = "7z.exe";

        public AutoExtractor()
        {
            InitializeComponent();
            
            String pauseBetweenChecks = ConfigurationSettings.AppSettings.Get("pauseBetweenChecks");
            checkFilesTimer.Interval = int.Parse(pauseBetweenChecks);
            notifyIcon.Icon = new Icon("idle.ico");

            srcDir = ConfigurationSettings.AppSettings.Get("SourceDir");
            dstDir = ConfigurationSettings.AppSettings.Get("DestDir");
            dstRoot = ConfigurationSettings.AppSettings.Get("DestRoot");
            if (!String.IsNullOrEmpty(dstRoot))
            {
                dstRootDirs = Directory.GetDirectories(dstRoot);
            }
            movieFileExtensions = ConfigurationSettings.AppSettings.Get("movieFileExtensions");

            
            ExtractFiles();
        }


        public void ExtractFiles()
        {
            checkFilesTimer.Enabled = false;

            if (notifyIcon.Text.Equals("") || notifyIcon.Text.StartsWith("Waiting"))
            {
                DebugMsg("Checking files in " + srcDir + ".");
                notifyIcon.Icon = new Icon("searching_folder.ico");
            }

            String[] rarFiles = Directory.GetFiles(srcDir, "*.rar");
            List<string> filesToExtract = new List<string>();
            for (int i=0;i<rarFiles.Length;i++)
            {
                if (!ShouldSkipRarFile(rarFiles[i]))
                {
                    filesToExtract.Add(rarFiles[i]);
                }
            }

            addPasswordToolStripMenuItem.Enabled = false;

            foreach (String filename in filesToExtract)
            {
                DebugMsg("Checking if " + GetFileFromPath(filename) + " is complete...");

                string rarPwd;
                string output;
                if (CanUnrarFile(filename, out rarPwd))
                {
                    int exitCode = UnrarFiles(filename, rarPwd, out output);

                    // Password protected rar with password on file rather than on file list - will be detected 
                    // only after extraction.
                    if (IsPasswordProtected(output))
                    {
                        DebugMsg("Trying passwords on file " + GetFileFromPath(filename));

                        List<string> passwords = GetPasswords();
                        for (int i=0; i < passwords.Count; i++)
                        {
                            exitCode = UnrarFiles(filename, passwords[i], out output);
                            if (!IsPasswordProtected(output))
                                break;
                        }
                    }

                    if (output.Contains(cannot_find_volume))
                    {
                        notifyIcon.Icon = new Icon("general_error.ico");
                        DebugMsg("Incomplete file " + GetFileFromPath(filename));
                    }
                    else if (IsPasswordProtected(output))
                    {
                        notifyIcon.Icon = new Icon("password_not_found.ico");
                        LogMsg("Password not found for " + GetFileFromPath(filename));
                    }
                    else if (exitCode == 0)
                    {
                        RenameDeleteFileAndLogSuccess(filename);
                    }
                    else
                    {
                        notifyIcon.Icon = new Icon("general_error.ico");
                        LogMsg("Error on " + GetFileFromPath(filename) + ". ExitCode=" + exitCode + ". " + output);
                    }
                }

            }


            String[] zipFiles = Directory.GetFiles(srcDir, "*.zip");
            foreach (String filename in zipFiles)
            {
                DebugMsg("Checking if " + GetFileFromPath(filename) + " contains substitles...");

                string output;
                if (ShouldUnzipFile(filename))
                {
                    int exitCode = UnzipSubtitles(filename, out output);

                    if (exitCode == 0)
                    {
                        RenameDeleteFileAndLogSuccess(filename);
                    }
                    else
                    {
                        notifyIcon.Icon = new Icon("general_error.ico");
                        LogMsg("Error on " + GetFileFromPath(filename) + ". ExitCode=" + exitCode + ". " + output);
                    }
                }

            }

            if (notifyIcon.Text.Contains("Checking files in "))
            {
                notifyIcon.Icon = new Icon("idle.ico");
                DebugMsg("Waiting....");
            }

            addPasswordToolStripMenuItem.Enabled = true;
            checkFilesTimer.Enabled = true;
        }

        private int UnzipSubtitles(string filename, out string output)
        {
            string subsFilename = "";
            string DestinationDir = dstDir;
            filename = "\"" + filename + "\"";

            string arguments = " l " + filename + " *.srt";
            RunShell(UNZIP_COMMAND, arguments, out output);
            string[] lines = output.Split('\r');
            for (int i=0; i<lines.Length; i++)
            {
                if (lines[i].Contains("Name") && lines[i].Contains("Time"))
                {
                    int pos = lines[i].IndexOf("Name");
                    subsFilename = lines[i + 2].Substring(pos);
                    break;
                }
            }

            string specificExtractionPath = GetSpecificExtractionPath(filename);
            if (!string.IsNullOrEmpty(specificExtractionPath))
            {
                DestinationDir = specificExtractionPath;
            }
            else
            {
                string cleanSubsFilename = subsFilename.Replace('.', ' ').Replace("\r", "").Replace("\n", "").Replace('-', ' ').ToLower();
                if (dstRootDirs != null)
                {
                    foreach (string dir in dstRootDirs)
                    {
                        string dirNameOnly = Path.GetFileName(dir);// dir.Substring(dstRoot.Length);
                        if (cleanSubsFilename.Contains(dirNameOnly.ToLower()))
                        {
                            DestinationDir = dir;
                            break;
                        }
                    }
                }
            }

            if (!DestinationDir.StartsWith("\""))
                DestinationDir = "\"" + DestinationDir + "\"";
            arguments = " e -o" + DestinationDir + " -y " + filename + " *.srt";
            return RunShell(UNZIP_COMMAND, arguments, out output);
        }

        private bool ShouldUnzipFile(string filename)
        {
            string output;

            filename = "\"" + filename + "\"";
            string arguments = " l " + filename + " *.srt";
            
            RunShell(UNZIP_COMMAND, arguments, out output);
            bool ret = !(output.Contains(" 0 files"));
            return ret;
        }

        private bool ShouldSkipRarFile(string filename)
        {
            if (filename.Contains(".part") && !filename.EndsWith(".part1.rar") && !filename.EndsWith("part01.rar"))
                return true;            

            string output;
            string arguments = "lb -v -p- " + filename + " " + movieFileExtensions;
            RunShell(UNRAR_COMMAND, arguments, out output);
            bool ret = (output.Trim(new[] {' ', '\t', '\n', '\r'}).Length == 0);
            return ret;
        }

        private bool IsPasswordProtected(string output)
        {
            return output.Contains(password_error_pattern);
        }

        private int UnrarFiles(string filename, string rarPwd, out string output)
        {
            string DestinationDir = dstDir;
            string pwdArg = rarPwd == null ? "" : "-p\"" + rarPwd + "\"";
            int exitCode;

            filename = "\"" + filename + "\"";

            // If the archive contains another archive - unrar it to src dir so in the next round the inner rar will be extracted
            string arguments = "lb -v -p- " + pwdArg + " " + filename + " " + movieFileExtensions;
            RunShell(UNRAR_COMMAND, arguments, out output);
            if (output.Contains(".rar\r\n"))
            {
                DestinationDir = GetPathOfFile(filename);
            }
            else
            {
                string specificExtractionPath = GetSpecificExtractionPath(filename);
                if (!string.IsNullOrEmpty(specificExtractionPath))
                {
                    DestinationDir = specificExtractionPath;
                }
                else if (dstRootDirs != null)
                {
                    string[] rarInternalFiles = output.Split(new[] {"\r\n"}, 5, StringSplitOptions.RemoveEmptyEntries);
                    foreach (string rarInternalFile in rarInternalFiles)
                    {
                        string foundDstDir = null;
                        foreach (string dir in dstRootDirs)
                        {
                            string dirNameOnly = Path.GetFileName(dir);// dir.Substring(dstRoot.Length);
                            string cleanRarFilename = rarInternalFile.Replace('.', ' ').Replace('-', ' ').ToLower();
                            if (cleanRarFilename.Contains(dirNameOnly.ToLower()))
                            {
                                foundDstDir = dir;
                                break;
                            }
                        }

                        if (foundDstDir != null)
                        {
                            DestinationDir = foundDstDir;
                            break;
                        }
                    }
                }
            }


            // Find small file inside the archive and try all passwords on it
            string smallFileFilename = "";
            // Keep the parameter vb - don't change it to vb or otherwise the extraction will fail
            arguments = "vb -v -p- -sl1000000 " + pwdArg + " " + filename + " *.???";
            exitCode = RunShell(UNRAR_COMMAND, arguments , out output);
            if (exitCode != 0 || output.Contains(cannot_find_volume))
                return exitCode;

            if (output.Contains("\n"))
            {
                smallFileFilename = "\"" + output.Substring(0, output.IndexOf("\n") - 1) + "\"";
            }
            if (!smallFileFilename.Equals(""))
            {
                arguments = "e -o+ -p- -v " + pwdArg + " " + filename + " " + smallFileFilename;
                exitCode = RunShell(UNRAR_COMMAND, arguments, out output);
            }

            if (exitCode != 0)
                return exitCode;

            if (!smallFileFilename.Equals(""))
            {
                File.Delete(Path.GetFileName(smallFileFilename.Replace("\"", "")));
            }

            notifyIcon.Icon = new Icon("extracting_files.ico");
            LogMsg("Extracting " + GetFileFromPath(filename) + " to " + DestinationDir + (rarPwd != null ? (" using password " + rarPwd) : ""));

            DestinationDir = "\"" + DestinationDir + "\"";
            arguments = "e -o+ -p- -v " + pwdArg + " " + filename + " " + DestinationDir;
            return RunShell(UNRAR_COMMAND, arguments, out output);
        }

        private string GetSpecificExtractionPath(string filename)
        {
            if (File.Exists("DestDirsAndPasswords.txt"))
            {
                string[] lines = File.ReadAllLines("DestDirsAndPasswords.txt");
                foreach (string line in lines)
                {
                    int pos = line.IndexOf(",");
                    string rsFilename = line.Substring(0, pos);
                    int pos2 = line.IndexOf(",", pos+1);
                    string dirName = line.Substring(pos+1, pos2 - pos - 1);
                    //string password = line.Substring(pos2 + 1);

                    string cleanFilename = filename.Substring(filename.LastIndexOf("\\") + 1);
                    cleanFilename = cleanFilename.Substring(0, cleanFilename.LastIndexOf("."));
                    if (rsFilename == cleanFilename)
                    {
                        return dirName;
                    }
                }
            }

            return null;
        }

        private void RenameDeleteFileAndLogSuccess(string filename)
        {
            string filenameWithoutPath = Path.GetFileNameWithoutExtension(filename);
            string filenameWithoutSuffix = filenameWithoutPath.Replace(".part1", "").Replace(".part01", "");//.Replace(".rar", "").Replace(".zip","");
            string suffix;
            if (filename.EndsWith(".part1.rar") || filename.EndsWith(".part01.rar"))
                suffix = ".part*.*";
            else if (filename.EndsWith(".zip"))
                suffix = ".zip";
            else
                suffix = ".r*";

            string allRarFiles = filenameWithoutSuffix + suffix;
            String[] rarFiles = Directory.GetFiles(srcDir, allRarFiles);
            foreach (string rarFile in rarFiles)
            {
                string targetFilename = rarFile + ".todel";
                if (File.Exists(targetFilename))
                    File.Delete(targetFilename);
                File.Move(rarFile, targetFilename);
            }

            LogMsg(GetFileFromPath(filename) + " extracted OK.");
            notifyIcon.Icon = new Icon("files_extracted.ico");
        }

        private bool CanUnrarFile(string filename, out string rarPwd)
        {
            string output;

            rarPwd = null;
            List<string> passwords = GetPasswords();

            // try without password
            int exitCode = CheckRar(filename, rarPwd, out output);
            if (exitCode == 0)
                return true;
            
            // if password required, try all passwords one after the other
            if (IsPasswordProtected(output))
            {
                DebugMsg("Trying passwords for " + GetFileFromPath(filename));
                for (int i=0; i<passwords.Count; i++)
                {
                    rarPwd = passwords[i];
                    exitCode = CheckRar(filename, rarPwd, out output);
                    if (!IsPasswordProtected(output))
                        break;
                }

                // password not found in passwords file
                if (IsPasswordProtected(output))
                {
                    notifyIcon.Icon = new Icon("password_not_found.ico");
                    LogMsg("Password not found for " + GetFileFromPath(filename));
                    return false;
                }
                    
            }

            if (output.Contains(cannot_find_volume))
            {
                notifyIcon.Icon = new Icon("general_error.ico");
                DebugMsg("Incomplete file " + GetFileFromPath(filename));
                return false;
            }

            if (exitCode == 0)
                return true;
            else
            {
                notifyIcon.Icon = new Icon("general_error.ico");
                LogMsg("Error on " + GetFileFromPath(filename) + ". ExitCode=" + exitCode + ". output=" + output);
                return false;
            }
        }

        private List<string> GetPasswords()
        {
            List<string> passwords = new List<string>();
            StreamReader pwds = new StreamReader("passwords.txt");

            string pwd = pwds.ReadLine();
            while (pwd != null)
            {
                passwords.Add(pwd);
                pwd = pwds.ReadLine();
            }
            pwds.Close();
            return passwords;
        }

        private int CheckRar(string filename, string password, out string output)
        {
            string pwdArg = password == null ? "" : "-p\"" + password + "\"";
            string arguments = "lb -p- -v "+ pwdArg + " " + filename;

            // UnRAR.exe lb -p- -p<pwd> -v <file.part1.rar>
            return RunShell(UNRAR_COMMAND, arguments, out output);
        }

        private int RunShell(string command, string arguments, out string output)
        {
            DebugMsg("Executing " + command + " " + arguments);
            Process p = new Process();
            // Redirect the output stream of the child process.
            p.StartInfo.UseShellExecute = false;
            p.StartInfo.RedirectStandardOutput = true;
            p.StartInfo.RedirectStandardError = true;
            p.StartInfo.FileName = command;
            p.StartInfo.Arguments = arguments;
            p.StartInfo.CreateNoWindow = true;
            p.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
            p.Start();
            // Do not wait for the child process to exit before
            // reading to the end of its redirected stream.
            // p.WaitForExit();
            // Read the output stream first and then wait.
            output = p.StandardOutput.ReadToEnd();
            output += p.StandardError.ReadToEnd();
            p.WaitForExit();

            return p.ExitCode;
        }

        private string getCurDateTime()
        {
            return /*DateTime.Now.ToShortDateString() + " " + */DateTime.Now.ToShortTimeString() + ":";
        }

        private void DebugMsg(String s)
        {
            String curTime = getCurDateTime();
            string msg = s.Substring(0, Math.Min(63 - curTime.Length, s.Length));
            log.Warn(s);
            notifyIcon.Text = msg;
        }

        private void LogMsg(String s)
        {
            DebugMsg(s);

            notifyIcon.BalloonTipText = s;
            notifyIcon.ShowBalloonTip(3000);
        }

        private void checkFilesTimer_Tick(object sender, EventArgs e)
        {
            ExtractFiles();
        }

        private void Exit_Click(object sender, EventArgs e)
        {
            Application.Exit();
        }

        private string GetFileFromPath(string fullPath)
        {
            return fullPath.Substring(fullPath.LastIndexOf("\\") + 1);
        }

        private string GetPathOfFile(string fullPath)
        {
            return fullPath.Substring(0, fullPath.LastIndexOf("\\"));
        }

        private void addPasswordToolStripMenuItem_Click(object sender, EventArgs e)
        {
            AddPassword addpwd = new AddPassword();
            DialogResult res = addpwd.ShowDialog();
            if (res == DialogResult.OK)
            {
                checkFilesTimer.Enabled = false;

                StreamWriter sw = new StreamWriter("passwords.txt", true);
                sw.WriteLine(addpwd.GetPwd());
                sw.Close();
                checkFilesTimer.Enabled = true;
            }
        }

        private void viewLogsToolStripMenuItem_Click(object sender, EventArgs e)
        {
            string dummy;
            RunShell("Notepad", "AutoExtractor-log.txt", out dummy);
        }

    }
}
